Lua 深度学习笔记

第一部分:Lua 是什么?(顶层认知)

1.1 一句话定义

Lua 是一种轻量级、快速、高效的编程语言。

但这太抽象了。让我们用更有趣的方式理解。

1.2 类比理解

如果Java是"办公楼"
  ├─ 功能完整
  ├─ 大而全
  ├─ 启动慢
  └─ 规则复杂

那么Lua就是"便利店"
  ├─ 功能基础
  ├─ 小而精
  ├─ 响应快
  └─ 规则简单

关键点: Lua 被设计成可以嵌入到其他程序中运行的脚本语言。

1.3 Lua 在产业中的位置

编程语言谱系图

                 系统语言
                    │
         ┌──────────┼──────────┐
         │          │          │
       C/C++      Java       Go
    (低阶,快速)  (中阶,通用)  (并发)

                脚本语言
                    │
         ┌──────────┼──────────┐
         │          │          │
      Python     JavaScript   Lua
    (通用脚本)  (Web脚本)   (嵌入脚本)

      Lua的核心特点:轻量级 + 可嵌入

第二部分:为什么需要 Lua?(问题导向)

2.1 现实场景分析

场景 1:在 Redis 中执行原子操作

问题: 我需要在 Redis 中做"读-判断-写"三个操作,必须不可分割(原子性)。

// Java中分开执行(不安全)
int count = redis.get("counter");  // 读
if (count > 10) {                  // 判断
    redis.set("flag", "yes");      // 写
}
// 问题:在读和写之间,其他线程可能修改了count
// 导致多个线程同时进入if,数据混乱

Lua 的解决方案: 把所有操作放在一个 Lua 脚本里,Redis 执行脚本时是原子的。

-- checkNeedCaptcha.lua
local count = redis.call('get', 'counter')
if count > 10 then
    redis.call('set', 'flag', 'yes')
    return 'true'
else
    redis.call('set', 'flag', 'no')
    return 'false'
end
-- Redis 保证整个脚本从开始到结束不会被中断

场景 2:减少网络往返(RTT)

问题: 如果分开执行,需要 3 次网络请求。

网络延迟时间线(RTT = Round Trip Time)

分开执行方式:
请求1: Java  Redis  Java (读)      耗时 T
请求2: Java  Redis  Java (判断)    耗时 T  (耗时 3T)
请求3: Java  Redis  Java (写)      耗时 T
总耗时: 3T

 Lua 脚本方式:
请求1: Java  Redis  Java (整个脚本)  耗时 T
总耗时: T

节省 66% 的网络开销!

场景 3:在 Redis 中完成复杂的业务逻辑

问题: 有些操作逻辑复杂,如果来回折腾网络太低效。

传统方式:
Java代码 ← 网络 → Redis ← 网络 → Java代码 ← 网络 → Redis ...
(多轮交互,耗时且出错风险高)

Lua方式:
Java代码 → Redis(一次)→ [Lua脚本完整执行] → Java代码
(一轮交互,快速且原子)

2.2 Lua 的核心优势总结

优势 说明 示例
原子性 脚本从开始到结束不会被中断 Redis 限流、抢购防超卖
减少 RTT 一次网络请求完成多个操作 省去多次 Redis 往返
内置Redis命令 能直接调用 redis.call() 不需要在 Java 和 Redis 之间来回
轻量级 文件小、启动快 脚本仅几KB,秒级加载
可嵌入 可集成到其他程序 Redis、Nginx、游戏引擎都内置Lua

第三部分:Lua 的语言设计(学习基础)

3.1 Lua 的设计哲学

Lua 的设计者说过一句名言:

"Do one thing, and do it well."

不要什么都会,但要把该会的事做好。

Lua设计原则:
┌─────────────────────────────────┐
│ 1. 简单易学 (语法少)            │
│    └─ 没有class、没有接口等    │
│                                 │
│ 2. 快速执行 (高效)              │
│    └─ 字节码+VM优化            │
│                                 │
│ 3. 轻量级 (资源少)              │
│    └─ Lua解释器仅440KB        │
│                                 │
│ 4. 可嵌入 (容易集成)            │
│    └─ C/C++可轻松调用         │
│                                 │
│ 5. 灵活 (适应性强)              │
│    └─ 动态类型、table等      │
└─────────────────────────────────┘

3.2 Lua 与其他语言的对比

语言特性对比表

         Lua      Python    Java
────────────────────────────────
简洁度    ★★★★★   ★★★★    ★★
学习难度  ★★      ★★★     ★★★★
执行速度  ★★★★   ★★★     ★★★★★
内存占用  ★★★★★  ★★★     ★★
集成难度  ★★     ★★★★    ★★★★
────────────────────────────────

结论:
Lua最适合"嵌入场景"

3.3 Lua 的核心数据结构

Lua 只有 一种复杂数据结构:Table(表)

-- 几乎所有复杂数据都是 Table

-- 1. 数组(Array)
local arr = {1, 2, 3, 4, 5}

-- 2. 字典(Dictionary / Hash Map)
local dict = {name = "Tom", age = 30}
-- 或者
local dict = {}
dict["key1"] = "value1"

-- 3. 对象(Object)- 本质还是 Table
local person = {
    name = "Tom",
    age = 30,
    greet = function(self)
        print("Hello, I'm " .. self.name)
    end
}

-- 4. 其他都是Table的变体

关键认知: Lua 用"简单"换"灵活"。没有专门的 class/interface,但 Table 足以表达一切。


第四部分:Lua 在 Redis 中的应用(实战重点)

4.1 Redis 如何执行 Lua 脚本

执行流程

┌──────────────────────────────────────────────────────────┐
│                     Java 应用端                           │
└────────────────────┬─────────────────────────────────────┘
                     │ 1. 加载脚本文件
                     │    (或直接传脚本文本)
                     │
        ┌────────────▼──────────────┐
        │ 2. 调用 redis.script.load │
        │    或 redis.script.evalsha│
        └────────────┬──────────────┘
                     │
                     │ 3. 网络传输
                     │    (脚本+参数)
                     │
┌────────────────────▼──────────────────────────────────────┐
│                    Redis 服务端                            │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐ │
│  │ 4. Lua虚拟机加载脚本                                 │ │
│  │    (Redis 内置的 Lua 5.1)                            │ │
│  └────────────────┬─────────────────────────────────────┘ │
│                   │                                        │
│  ┌────────────────▼─────────────────────────────────────┐ │
│  │ 5. 执行脚本逻辑                                       │ │
│  │    • 解析KEYS参数                                   │ │
│  │    • 解析ARGV参数                                   │ │
│  │    • 调用redis.call()操作                           │ │
│  │    • 执行业务逻辑(if/for等)                         │ │
│  │    • 返回结果                                       │ │
│  │                                                      │ │
│  │    【关键】所有操作在一个事务中 → 原子性保证        │ │
│  └────────────────┬─────────────────────────────────────┘ │
│                   │                                        │
│  ┌────────────────▼─────────────────────────────────────┐ │
│  │ 6. 返回结果给 Java                                   │ │
│  │    (可以是 string/number/table)                     │ │
│  └────────────────┬─────────────────────────────────────┘ │
└────────────────────┬──────────────────────────────────────┘
                     │
                     │ 7. Java接收结果
                     ▼
        ┌──────────────────────────┐
        │   继续业务逻辑处理        │
        └──────────────────────────┘

4.2 关键概念:EVALSHA vs EVAL

两种执行方式对比

方式1:EVAL (传脚本文本)
Java →  redis.eval("local x = ...", keys, args)
        │
        └─ 优点:简单直观
           缺点:每次都传脚本,网络浪费

方式2:EVALSHA (传脚本哈希)
步骤1:redis.script.load(脚本)  ← 加载一次,返回SHA1
步骤2:redis.evalsha(SHA1, keys, args)  ← 后续都用哈希
        │
        ├─ 优点:网络高效,脚本只传一次
        └─ 缺点:需要先加载

在高并发场景,通常用 EVALSHA:
  • 初始化时 script.load() 一次
  • 后续直接 evalsha(),节省网络

4.3 Lua 与 Redis 命令的交互

-- checkNeedCaptcha.lua 中的典型交互

-- 1. 读数据
local count = redis.call('get', counter_count_key)

-- 2. 写数据
redis.call('set', counter_count_key, count)

-- 3. 设置过期时间
redis.call('expire', verify_captcha_id, 60)

-- 4. 条件判断(Lua语言,不是Redis命令)
if count > 10 then
    redis.call('set', flag, 'yes')
else
    redis.call('set', flag, 'no')
end

-- 5. 返回结果给Java
return 'true'  -- 或 'false'

关键分工:

  • Lua语言部分(if/for/等):脚本逻辑
  • redis.call():调用Redis命令
  • 原子性:整个脚本执行过程中,其他客户端的请求被暂停

第五部分:Lua 脚本的原子性保证(核心价值)

5.1 为什么原子性很重要?

问题场景:没有原子性

// Java 中分开执行(非原子)
int count = redis.get("counter");      // 读:count=10

// 【时间点A】其他线程可能已修改 counter

if (count > 10) {                       // 判断:10 > 10? false
    redis.set("flag", "yes");          // 不执行
}
redis.set("flag", "no");               // 执行

// 问题:虽然我们在Java里判断是<=10,
// 但实际上Redis中的counter可能已经是20了
// 逻辑产生了矛盾!

解决方案:用 Lua 脚本(原子)

-- 整个脚本从START到END,其他客户端看不到中间过程
-- 要么全部执行,要么全部不执行

-- START ─────────────────────────────────────
local count = redis.call('get', 'counter')  -- 读
if count > 10 then                          -- 判断
    redis.call('set', 'flag', 'yes')       -- 写
else
    redis.call('set', 'flag', 'no')        -- 写
end
return 'done'
-- END ───────────────────────────────────────

-- 【保证】上面的所有操作,对其他客户端是透明的
-- 其他客户端要么看到执行前,要么看到执行后
-- 看不到中间过程

5.2 原子性的实现机制

Redis的单线程架构 + Lua脚本

Redis服务端(单线程)

时间线:
T1: 客户端1 发来 redis.set('a', 1)
    ├─ Redis执行
    ├─ 完成 
    
T2: 客户端2 发来 Lua脚本(10行)
    ├─ Redis 加载脚本到虚拟机
    ├─ 执行第1行 (redis.call('get', 'x'))
    ├─ 执行第2行 (if判断)
    ├─ 执行第3行 (redis.call('set', 'y', ...))
    ├─ ...
    ├─ 执行第10行 (return)
    ├─ 脚本执行完成 
     【关键】中间没有其他客户端的命令插入!
    
T3: 客户端3 发来 redis.get('y')
    ├─ Redis执行
    ├─ 完成 

结论: Redis单线程 + Lua脚本组合 = 天然原子性

5.3 对比:Java中如何实现类似的原子性

// 方式1:使用同步块(不推荐,在应用层)
private Object lock = new Object();
synchronized(lock) {
    int count = redis.get("counter");
    if (count > 10) {
        redis.set("flag", "yes");
    } else {
        redis.set("flag", "no");
    }
}
// 问题:
// 1. 只能保护Java的操作,Redis还是并发的
// 2. 锁持续时间长(包括网络延迟),竞争激烈
// 3. 多个Java实例无法共享锁

// 方式2:使用Redis Lua脚本(推荐!)
redis.evalsha(scriptSha, keys, args);
// 优点:
// 1. 保护了Redis操作(真正的原子)
// 2. 锁持续时间短(脚本执行时间毫秒级)
// 3. 全局有效(多实例共享Redis)

第六部分:大麦网案例深度解析

6.1 checkNeedCaptcha.lua 为什么需要 Lua?

场景:全局限流,决定是否需要验证码

需求分析:
┌─────────────────────────────────────────┐
│ 1. 读取全局计数器                      │
│ 2. 获取上次重置时间                    │
│ 3. 判断是否过了1秒                     │
│ 4. 如果过了,重置计数器                │
│ 5. 对计数器+1                          │
│ 6. 判断是否超过阈值(10)                │
│ 7. 根据结果,写入标记('yes'或'no')    │
│ 8. 返回结果给Java                      │
└─────────────────────────────────────────┘

如果分开执行(Java+Redis多次往返):

        Java代码               Redis

        ├─ 发命令1 ────────────>
        │ GET COUNTER_COUNT
        │<────────────────────(网络延迟)
        │
        ├─ 发命令2 ────────────>
        │ GET COUNTER_TIMESTAMP
        │<────────────────────(网络延迟)
        │
        ├─ 【Java判断逻辑】
        │ (计算count, 判断1秒)
        │
        ├─ 发命令3 ────────────>
        │ SET COUNTER_COUNT
        │<────────────────────(网络延迟)
        │
        ├─ 发命令4 ────────────>
        │ SET VERIFY_CAPTCHA_ID
        │<────────────────────(网络延迟)

总耗时:网络延迟 × 4 = 4T(非常慢!)

用 Lua 脚本执行:

        Java代码               Redis

        ├─ 发脚本 ────────────>
        │ (包含所有逻辑)
        │
        │<────────────────────(一次往返)
        │ 脚本执行完,返回结果

总耗时:网络延迟 × 1 = T(快4倍!)

而且【原子性】保证:
- 读数据、判断、写标记 一气呵成
- 其他请求看不到中间过程
- 防止了并发竞争导致的计数错误

6.2 Lua 脚本的结构分析

-- 第1部分:接收参数
local counter_count_key = KEYS[1]
local verify_captcha_threshold = tonumber(ARGV[1])

-- 第2部分:强制开启检查(紧急防御)
if always_verify_captcha == 1 then
    redis.call('set', verify_captcha_id, 'yes')
    return 'true'
end

-- 第3部分:获取当前状态(读操作)
local count = tonumber(redis.call('get', counter_count_key) or "0")
local lastResetTime = tonumber(redis.call('get', counter_timestamp_key) or "0")

-- 第4部分:时间窗口处理(业务逻辑)
if current_time_millis - lastResetTime > 1000 then
    count = 0
    redis.call('set', counter_count_key, count)
end

-- 第5部分:阈值判定(核心逻辑)
count = count + 1
if count > verify_captcha_threshold then
    redis.call('set', verify_captcha_id, 'yes')
    return 'true'
else
    redis.call('set', verify_captcha_id, 'no')
    return 'false'
end

为什么这个逻辑不能在Java中做?

关键原因:Redis 中的操作不能中断

如果逻辑分散在Java和Redis中:

时刻T1:
  Java获取count=10

时刻T2:
  【其他客户端发来请求】
  Redis的count变为11、12、13...
  Java还在做判断...

时刻T3:
  Java判断:count(10) > 10? No
  redis.set(flag, 'no')

问题:实际count已经是13了,但我们设置的是'no'
     逻辑错误!

用Lua脚本,整个过程是原子的:

时刻T1:
  [Lua脚本开始执行]
  ├─ 读count=10
  ├─ 【其他客户端请求来临,但被暂停】
  ├─ count+1=11
  ├─ 判断11 > 10? Yes
  ├─ 设置标记'yes'
  [Lua脚本结束]

时刻T2:
  【其他客户端的请求现在才执行】

保证逻辑正确!

第七部分:Lua 实战最佳实践

7.1 何时该用 Lua?(决策树)

              需要在Redis中执行操作吗?
                      │
                     是
                      │
        ┌─────────────┴─────────────┐
        │                           │
    【只用一条命令?】          【需要多条命令】
    /              \                 │
  是               否         【命令间有依赖关系?】
   │                          /              \
   │                        是              否
   │                        │                │
  不用Lua          【需要原子性?】      可以分开执行
  直接用Redis        /        \           不用Lua
  命令              是        否
   │                │         │
用Lua!        用Lua!    一般不用Lua
(高效)     (推荐)     (e.g.日志)
  实践判断标准:
┌────────────────────────────────────────┐
│ ✓ 用 Lua 的场景(高优先级)          │
│                                        │
│ 1. 需要原子性 → 限流、库存、抢购     │
│ 2. 多条命令相互依赖 → 读-判-写      │
│ 3. 减少网络往返 → 高并发场景        │
│ 4. 业务逻辑复杂 → 多个判断分支     │
│ 5. 数据一致性关键 → 金融场景        │
│                                        │
│ 例子:verify_captcha, 防超卖, 库存减  │
└────────────────────────────────────────┘
 ┌────────────────────────────────────────┐
│ ✗ 不用 Lua 的场景                      │
│                                        │
│ 1. 简单操作 → SET/GET/INCR            │
│ 2. 独立操作 → 日志、统计计数           │
│ 3. 逻辑全在应用层 → 数据处理          │
│ 4. 网络不敏感 → 低频操作               │
│                                        │
│ 例子:用户点赞数+1, 日志记录          │
└────────────────────────────────────────┘

7.2 开发流程:从编写到运行

Step 1: 编写脚本文件
────────────────────
目录结构:
src/main/resources/
└── lua/
    └── checkNeedCaptcha.lua
 脚本内容:
local count = redis.call('get', KEYS[1]) or 0
count = count + 1
redis.call('set', KEYS[1], count)
...
  Step 2: Spring 初始化加载
────────────────────────
@Component
public class CheckNeedCaptchaOperate {

    private DefaultRedisScript<String> redisScript;

    @PostConstruct
    public void init() {
        redisScript = new DefaultRedisScript<>();
        // 从classpath加载脚本
        redisScript.setScriptSource(
            new ResourceScriptSource(
                new ClassPathResource("lua/checkNeedCaptcha.lua")
            )
        );
        // 指定返回类型
        redisScript.setResultType(String.class);
    }
}
 好处:
  • 项目启动时加载一次(1ms)
  • 后续使用EVALSHA,只需传哈希(高效)
  • 避免每次都读文件
  Step 3: 执行脚本
────────────────
public Boolean checkNeedCaptcha(List<String> keys, String[] args) {
    // 调用脚本
    Object result = redisCache.getInstance()
        .execute(redisScript, keys, args);

    // 结果处理
    return Boolean.parseBoolean((String)result);
}
 参数说明:
  keys  → Redis Key列表 [counter_count, counter_timestamp, verify_id]
  args  → Lua参数列表 [10, 1700000000123, 60, 0]
  result → Lua脚本返回值
  Step 4: 集成测试
────────────────
@Test
public void testCheckNeedCaptcha() {
    // 准备参数
    List<String> keys = Arrays.asList("counter", "timestamp", "flag_123");
    String[] args = {"10", "1700000000000", "60", "0"};

    // 执行脚本
    Boolean result = captchaOperate.checkNeedCaptchaOperate(keys, args);

    // 验证结果
    assertTrue(result);  // 应该返回true或false

    // 验证Redis中的值
    String flagValue = redis.get("flag_123");
    assertEquals("yes", flagValue);
}

7.3 常见坑与避坑指南

❌ 坑1:脚本太长太复杂(难以维护)
   症状:脚本500行,if嵌套20层
   原因:试图在脚本中完成所有业务逻辑
   解决:
   • 保持脚本<100行
   • 复杂逻辑放在Java中
   • Lua只做"原子操作"部分

   正确方式:
   Lua脚本: 只做 读--写 的原子化
   Java代码: 负责复杂的业务逻辑、数据转换

  ❌ 坑2:脚本返回复杂数据结构(序列化困难)
   症状:return {user={name="Tom", age=30}}
   问题:Lua的table转Java的Object很复杂
   解决:
   • 只返回基本类型(string/number)
   • 复杂数据在Java层组装

   好的实践:
   return 'true'    -- 字符串
   return '{"code":"ok"}'  -- JSON字符串(Java parse)
   return 100       -- 数字

   不好的实践:
   return {code=0, data={...}}  -- 复杂table

  ❌ 坑3:忘记设置Key过期时间(内存泄漏)
   症状:Redis内存不断增长
   原因:created verify_captcha_id_123456 但没有EXPIRE
   解决:
   redis.call('expire', key, ttl)  -- 必须加!

   完整模板:
   redis.call('set', myKey, myValue)
   redis.call('expire', myKey, 60)  -- 60秒后自动删除

  ❌ 坑4:脚本中有网络操作或阻塞操作(锁死Redis)
   症状:Redis响应变慢,所有请求堆积
   原因:脚本中调用HTTP接口、数据库等耗时操作
   解决:禁止!脚本只能做本地运算和Redis操作

   禁止清单:
   ✗ redis.call('HTTP_GET', url)  -- 不存在
   ✗ redis.call('SELECT FROM db...') -- 不存在
   ✗ sleep(1000)  -- 会锁死
   ✗ io.open('file.txt')  -- 文件I/O

   允许清单:
   ✓ redis.call('GET', key)  -- Redis操作
   ✓ math.max(a, b)  -- 本地运算
   ✓ string.format()  -- 字符串操作table operations  -- 表操作

  ❌ 坑5:没有考虑脚本版本管理(升级困难)
   症状:脚本更新后,老客户端仍用老版本
   原因:没有版本控制机制
   解决:
   • 使用EVALSHA方式,脚本哈希包含版本信息
   • 发布脚本时做好日志记录
   • 支持脚本灰度升级

   版本管理方案:
   v1: checkNeedCaptcha_v1.lua (SHA1: abc123...)
   v2: checkNeedCaptcha_v2.lua (SHA1: def456...)

   启动时:
   scriptSha_v2 = redis.script.load(v2_content)
   # 逐步升级,保证兼容性

  ❌ 坑6:脚本执行超时(Redis卡顿)
   症状:Redis管理员告诉你"你的脚本运行1分钟!"
   原因:脚本中有死循环或过度复杂
   解决:
   • 控制脚本逻辑复杂度
   • 不要在脚本中做大规模数据处理

   Redis脚本限制:
   默认超时 = 5秒
   超时后 → Redis强制杀死脚本

   best practice:
   脚本执行时间应该 < 100ms (毫秒级)
  ✓ 正确做法总结:
  1. 脚本简洁 (<100行)
  2. 只做原子操作
  3. 返回基本类型
  4. 记得设置过期时间
  5. 禁止网络/IO操作
  6. 做好版本管理
  7. 监控脚本执行时间

第八部分:Lua vs 其他技术方案(对比分析)

8.1 原子性问题的5种解决方案

需求:在分布式系统中安全地执行"扣款100元"
 方案1:Java synchronized(不推荐)
┌──────────────────────────────────────┐
│ synchronized(lock) {                 │
│   int balance = redis.get("account"); │
│   if (balance >= 100) {              │
│     redis.set("account", balance-100);│
│   }                                  │
│ }                                    │
└──────────────────────────────────────┘
分析:
  ✗ 只保护单机Java代码
  ✗ Redis操作仍并发
  ✗ 多实例互不影响,全局不安全
  ✗ 锁持续时间长(包含网络延迟)
  ✗ 性能差,竞争激烈

适用场景:单机应用(现在几乎没人用)
  方案2:数据库事务(传统方案)
┌──────────────────────────────────────┐
│ @Transactional                       │
│ public void debit() {                │
│   Account acc = query("123");        │
│   acc.balance -= 100;                │
│   save(acc);                         │
│ }                                    │
└──────────────────────────────────────┘
分析:
  ✓ 保证数据库级别原子性
  ✓ 安全可靠
  ✗ 每次都打数据库,网络往返多
  ✗ 数据库连接池压力大
  ✗ 高并发时容易超时

适用场景:小并发、强一致性要求高
       不适合高并发实时场景
  方案3:分布式锁 (Redlock)
┌──────────────────────────────────────┐
│ lock = redlock.lock("account_123");  │
│ try {                                │
│   balance = redis.get("account");    │
│   redis.set("account", balance-100); │
│ } finally {                          │
│   lock.unlock();                     │
│ }                                    │
└──────────────────────────────────────┘
分析:
  ✓ 跨实例生效
  ✓ Redis层面同步
  ✗ Redlock实现复杂,易出bug
  ✗ 多线程竞争,吞吐量受限
  ✗ 仍需多次网络往返
  ✗ 存在死锁、超时等问题

适用场景:需要分布式锁的通用场景
       但不是最优解
  方案4:消息队列 (Queue)
┌──────────────────────────────────────┐
│ // 生产者                             │
│ queue.send("debit_msg", {            │
│   account_id: "123",                 │
│   amount: 100                        │
│ });                                  │
│                                      │
│ // 消费者(单线程处理)              │
│ msg = queue.consume();               │
│ balance = redis.get(account_id);     │
│ redis.set(account_id, balance-100);  │
└──────────────────────────────────────┘
分析:
  ✓ 保证顺序性
  ✓ 天然的原子操作(单线程消费)
  ✗ 有延迟(消息在队列中等待)
  ✗ 复杂(需要额外的MQ系统)
  ✗ 难以实时反馈结果给用户

适用场景:批量处理、异步操作
       不适合实时扣款
  方案5:Redis Lua脚本(最优!✓✓✓)
┌──────────────────────────────────────┐
│ redis.evalsha(                       │
│   "def123...",                       │
│   ["account_123"],                   │
│   ["100"]                            │
│ );                                   │
│                                      │
│ -- Lua脚本:                         │
│ local balance = redis.call('GET',..);│
│ if balance >= 100 then               │
│   redis.call('SET',...,balance-100); │
│   return 'success'                   │
│ end                                  │
└──────────────────────────────────────┘
分析:
  ✓ Redis原生支持,无额外复杂度
  ✓ 一次网络请求完成原子操作
  ✓ 脚本简洁优雅
  ✓ 性能最高
  ✓ 天然的分布式安全
  ✓ 实时返回结果
  ✓✓✓ 高并发必选!

适用场景:高并发、实时、强一致性
       限流、防超卖、库存系统
  【结论】
方案1(synchronized) ← 不用
方案2(DB事务) ← 小并发
方案3(Redlock) ← 通用分布式
方案4(MQ) ← 异步场景
方案5(Lua) ← 高并发实时(最优选择!)
 大麦网选择Lua的理由:
  • 高并发秒杀场景
  • 需要真正的原子性
  • 实时反馈验证结果
  • 性能要求极高
  → Lua完美匹配!

8.2 执行效率对比

假设需要3个操作(READ → JUDGE → WRITE)
网络延迟 = 1ms/次
Redis执行时间 = 0.1ms
 方案        操作流程                    总耗时    网络往返
──────────────────────────────────────────────────────
synchronized
            read(1ms)
            judge(无网络)              3.1ms    3次
            write(1ms)

DB事务      query db(5ms)
            compute(无网络)            15.1ms   3次
            update db(5ms)
 Redlock     lock(1ms)
            read(1ms)                  4.2ms    4次
            write(1ms)
            unlock(1ms)
 MQ          send msg(1ms)
            consume & process(50ms)    51ms     2次
            [有延迟!]
 Lua脚本     evalsha(1ms)
            [脚本内:读-判-写]          1.3ms    1次
            ← 最快!
  性能对比图:
 耗时 (ms)
│
│  ■ Lua脚本  1.3ms ■
│  ■ Redlock  4.2ms ■■
│  ■ synch... 3.1ms ■■
│  ■ MQ       51ms  ■■■■■■■■■■■■
│  ■ DB事务   15.1ms ■■■■
│
└────────────────────
 Lua 的优势:
  • 网络往返最少(只需1次)
  • 执行时间最短(1.3ms)
  • 吞吐量最高(可达万级QPS)

第九部分:Lua 的扩展应用(眼界开阔)

9.1 Lua 在其他领域的应用

Lua不只在Redis中应用,它是一种通用脚本语言
 ┌──────────────────────────────────────────────────┐
│ 1. 游戏引擎(最广泛应用)                        │
│                                                  │
│   • Unity → C# (不用Lua)                        │
│   • Cocos2d-x → Lua (热门引擎)                 │
│   • Love2D → Lua原生                           │
│   • 王者荣耀、梦幻西游都用Lua写业务逻辑       │
│                                                  │
│   优势:可以热更新(不用重编译)                 │
│         这对游戏运营很重要                      │
└──────────────────────────────────────────────────┘
 ┌──────────────────────────────────────────────────┐
│ 2. Web服务器 (Nginx)                            │
│                                                  │
│   nginx.conf:                                   │
│   location /api/check {                         │
│       content_by_lua_block {                    │
│           if ngx.var.request_method == "POST" then
│               local body = ngx.req.read_body()  │
│               return ngx.say("ok")              │
│           end                                   │
│       }                                         │
│   }                                             │
│                                                  │
│   用途:网关层快速处理、限流、鉴权              │
│   优势:避免请求到达Java,在网关层直接过滤    │
└──────────────────────────────────────────────────┘
 ┌──────────────────────────────────────────────────┐
│ 3. 配置管理 (Ansible)                           │
│                                                  │
│   playbook.yml:                                 │
│   tasks:                                        │
│     - name: Execute config script              │
│       script: config.lua                       │
│       args:                                     │
│         - "production"                         │
│                                                  │
│   用途:运维自动化脚本                         │
└──────────────────────────────────────────────────┘
 ┌──────────────────────────────────────────────────┐
│ 4. 数据库脚本 (Tarantool)                       │
│                                                  │
│   Tarantool = Redis + Lua + DB                  │
│   内存数据库,原生支持Lua,比Redis功能更强    │
│   用于超高并发场景                             │
└──────────────────────────────────────────────────┘
 ┌──────────────────────────────────────────────────┐
│ 5. 智能家居 (NodeMCU)                           │
│                                                  │
│   ESP8266芯片运行Lua                            │
│   开发IoT设备的首选语言                        │
└──────────────────────────────────────────────────┘

9.2 Redis 高级 Lua 应用示例

应用1:库存扣减(防超卖)

-- inventory_deduct.lua
-- 安全地扣减库存,防止超卖
 local key = KEYS[1]           -- 商品ID
local amount = tonumber(ARGV[1])  -- 购买数量
 local current = tonumber(redis.call('GET', key) or "0")
 if current >= amount then
    redis.call('DECRBY', key, amount)
    return 'success'
else
    return 'insufficient'
end

应用2:限流通道(令牌桶)

-- rate_limit.lua
-- 令牌桶算法实现
 local rate_key = KEYS[1]
local rate_limit = tonumber(ARGV[1])  -- 100 tokens/sec
local current_tokens = tonumber(redis.call('GET', rate_key) or rate_limit)
 if current_tokens > 0 then
    redis.call('DECR', rate_key)
    redis.call('EXPIRE', rate_key, 1)
    return 'allowed'
else
    return 'blocked'
end

应用3:分布式会话锁

-- distributed_lock.lua
-- 用Lua实现简单的分布式锁
 local lock_key = KEYS[1]
local lock_id = ARGV[1]
local expire_time = tonumber(ARGV[2])
 -- 原子地检查并设置
if redis.call('EXISTS', lock_key) == 0 then
    redis.call('SET', lock_key, lock_id)
    redis.call('EXPIRE', lock_key, expire_time)
    return 'locked'
else
    return 'already_locked'
end

这些都是Lua在实战中的真实应用。


第十部分:学习总结与进阶方向

10.1 Lua 学习脑图

         ┌─── Lua是什么
         │    ├─ 轻量级脚本语言
         │    ├─ 可嵌入式设计
         │    └─ 简洁高效
         │
Lua掌握 ─┼─── 为什么用Lua
         │    ├─ 原子性保证
         │    ├─ 减少网络往返
         │    └─ 业务逻辑聚合
         │
         ├─── Lua基础语法
         │    ├─ 数据类型(Table)
         │    ├─ 流程控制(if/for)
         │    └─ 函数定义
         │
         ├─── Redis中的应用
         │    ├─ redis.call()
         │    ├─ EVAL vs EVALSHA
         │    └─ 脚本加载
         │
         ├─── 实战优化
         │    ├─ 脚本简洁化
         │    ├─ 参数配置化
         │    └─ 性能监控
         │
         └─── 高级应用
              ├─ 分布式锁
              ├─ 限流算法
              └─ 库存管理

10.2 进阶学习路线

Level 1: 入门 (已掌握)
  □ Lua语法基础
  □ Redis.call()使用
  □ 简单的限流脚本
 Level 2: 中级 (下一步)
  □ 脚本优化(减少网络往返)
  □ 脚本版本管理
  □ 数据结构应用(集合、排序集)
  □ 错误处理
 Level 3: 高级 (长期目标)
  □ 复杂算法实现(HyperLogLog、Bloom Filter)
  □ 多Lua脚本协作
  □ Lua + Nginx 网关层应用
  □ Tarantool(Redis++)
 Level 4: 架构 (深度应用)
  □ 高并发系统设计
  □ 库存管理系统
  □ 实时计费系统
  □ 分布式事务

10.3 常用资源

推荐学习资源:
 官方文档:
  • Redis Lua脚本官方文档
    https://redis.io/commands/eval
 Lua语言教程:
  • Lua官网: https://www.lua.org/
  • Lua 5.1参考手册
 实战参考:
  • 大麦网源码(本项目)
  • Redis官方示例库
  • Nginx + Lua教程
 工具支持:
  • Redis CLI (redis-cli EVAL)
  • Lua IDE (ZeroBrane Studio)
  • 在线Lua编译器
 进阶书籍:
  • 《Redis深度历险》
  • 《Redis开发与运维》
  • 《Nginx高性能Web服务器》

第十一部分:核心要点回顾(速记)

11.1 一张表总结 Lua

维度          核心要点
────────────────────────────────────────
是什么        轻量级脚本,可嵌入到Redis中
 为什么用      ✓ 原子性(不会被中断)
             ✓ 低延迟(一次网络往返)
             ✓ 高效率(减少RTT)
 怎么用        redis.evalsha(脚本, KEYS, ARGV)

何时用        需要原子性 + 多个Redis操作
 关键特性      ├─ redis.call()调用命令
             ├─ 脚本从START到END无中断
             ├─ 基于Redis单线程
             └─ 返回简单类型给Java
 常见模式      ├─ 限流(阈值判定)
             ├─ 防超卖(库存检查)
             ├─ 分布式锁(互斥)
             └─ 采样触发(柔性防护)
 避坑指南      ├─ 脚本<100行(保持简洁)
             ├─ 禁止网络操作(会锁死)
             ├─ 记得设置过期(防泄漏)
             └─ 返回基本类型(易转换)
 性能指标      ├─ 网络往返: 1次
             ├─ 执行时间: <100ms
             ├─ 吞吐量: 万级QPS
             └─ 原子性: 100%保证

11.2 快速判断是否需要 Lua

问题            判断          方案
────────────────────────────────────
单个Redis操作?  是            不用Lua
                否
多个操作独立?   是            不用Lua
                否
需要原子性?     是    ───→   用Lua ✓
                否
业务逻辑简单?   是            可以不用
                否    ───→   用Lua ✓
 快速判断:
"我需要在Redis中原子地执行多个操作吗?"
    是 → 用Lua
    否 → 不用

11.3 Lua 的"一图流"记忆法

                【Java应用】
                    │
                    │ EVALSHA
                    ▼
           ┌─────────────────┐
           │  Redis单线程    │
           │  ┌───────────┐  │
           │  │ Lua虚拟机 │  │
           │  │ ┌─────────┴─┐│
           │  │ │脚本执行   ││
           │  │ │原子操作   ││
           │  │ │无中断    ││
           │  │ └─────────┬─┘│
           │  └───────────┘  │
           │       │返回结果  │
           └────────┬────────┘
                    │
              ┌─────▼─────┐
              │返回值给Java│
              └───────────┘
  记忆关键:
- 单线程 + 脚本 = 原子性
- 原子性 + 高效 = 解决高并发
- 一次往返 + 无等待 = 极速体验

总结:为什么大麦网选择 Lua

回到原点,理解大麦网为什么在全局流量识别中使用 Lua:

高并发秒杀场景:
  ├─ 1秒内可能有1万个注册请求
  ├─ 需要快速判断:是否需要验证码
  ├─ 决策依据:全局计数器值
  └─ 结果:写入标记供下步使用
 如果不用Lua:
  1秒内1万个请求
    ↓ (每个请求都读Redis)
  1万次网络往返
    ↓ (每次1ms延迟)
  总耗时 10秒! ✗
 用了Lua:
  1秒内1万个请求
    ↓ (用EVALSHA执行脚本)
  执行原子逻辑(在Redis内部)
  计数准确,原子性保证
    ↓ (EVALSHA只需1ms)
  1秒完成 ✓
 这就是Lua被广泛应用在高并发系统中的根本原因!

最后:一句话的精髓

Lua 是高并发系统的"快速通道":

把多个操作合并成一个原子脚本,在 Redis 内部一次性执行,避免来回网络开销,保证数据一致性。

这就是 Lua 的全部精华!


企业级项目导航:⬅️ 05-JWT + Redis 双重会话管理 学习笔记 | 06-Lua 深度学习笔记 | ➡️ 07-敏感数据保护:展示脱敏与加密存储深度解析